// SendWeChatCardMessageServerCommand.cs
// SendWeChatCardMessageServerCommand.cs
// SendWeChatCardMessageServerCommand.cs

using GrapeCity.Forguncy.Commands;
using GrapeCity.Forguncy.Plugin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace SendWeChatCardMessage
{
    [Icon("pack://application:,,,/SendWeChatCardMessage;component/Resources/Icon.png")]
    [OrderWeight(1)]
    [Category("企业微信")]
    public class SendWeChatCardMessageServerCommand : Command, ICommandExecutableInServerSideAsync
    {
        #region 设计时属性
        [ComboProperty(ValueList = "text|image|voice|video|file|textcard|news|markdown|miniprogram_notice",
                     DisplayList = "文本|图片|语音|视频|文件|文本卡片|图文|Markdown|小程序通知")]
        [DisplayName("消息类型")]
        [Required]
        public string Msgtype { get; set; } = "text";

        [FormulaProperty]
        [Required]
        [DisplayName("企业ID")]
        public object CorpId { get; set; } = "";

        [FormulaProperty]
        [Required]
        [DisplayName("应用密钥")]
        public object Secret { get; set; } = "";

        [FormulaProperty]
        [Required]
        [DisplayName("应用AgentId")]
        public object AgentId { get; set; } = "";

        [FormulaProperty]
        [DisplayName("接收人Id")]
        [Description("多个接收人用|连接")]
        public object Touser { get; set; } = "";

        [FormulaProperty]
        [DisplayName("接收部门Id")]
        public object Toparty { get; set; }

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("文本内容")]
        public string TextContent { get; set; } = "";

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("Markdown 内容")]
        public string MarkdownContent { get; set; } = "";

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("小程序通知标题")]
        public string MiniTitle { get; set; } = "";

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("小程序页面路径")]
        public string MiniPage { get; set; } = "pages/index/index";

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("标题")]
        public string CardTitle { get; set; } = "";

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("描述")]
        public string CardDesc { get; set; } = "";

        [TextProperty(AcceptsReturn = true)]
        [DisplayName("跳转链接")]
        public string CardUrl { get; set; } = "https://work.weixin.qq.com";

        [TextProperty(AcceptsReturn = true)]
        [Description("每行 title|desc|url|picurl")]
        [DisplayName("图文内容")]
        public string NewsLines { get; set; } = "";

        [BoolProperty]
        [DisplayName("使用图片媒体文件Id")]
        public bool UseMediaId { get; set; } = false;

        [FormulaProperty]
        [DisplayName("文件路径(含扩展名)")]
        public object FilePath { get; set; } = "";

        [FormulaProperty]
        [DisplayName("图片媒体文件Id")]
        public object MediaId { get; set; } = "";

        [ResultToProperty]
        [DisplayName("返回的结果")]
        public string ResultTo { get; set; } = "结果";
        #endregion

        #region 设计器可见性
        public override bool GetDesignerPropertyVisible(string propertyName, CommandScope commandScope)
        {
            var t = Msgtype?.ToLower();
            return propertyName switch
            {
                nameof(TextContent) => t == "text",
                nameof(MarkdownContent) => t == "markdown",
                nameof(MiniTitle) or nameof(MiniPage) => t == "miniprogram_notice",
                nameof(CardTitle) or nameof(CardDesc) or nameof(CardUrl) => t == "textcard",
                nameof(NewsLines) => t == "news",
                nameof(UseMediaId) => new[] { "image", "voice", "video", "file" }.Contains(t),
                nameof(FilePath) => new[] { "image", "voice", "video", "file" }.Contains(t) && !UseMediaId,
                nameof(MediaId) => new[] { "image", "voice", "video", "file" }.Contains(t) && UseMediaId,
                _ => base.GetDesignerPropertyVisible(propertyName, commandScope)
            };
        }
        #endregion

        #region 运行时校验
        private static void Validate(string msgType,
                                   string textContent, string markdownContent,
                                   string cardTitle, string cardUrl, string newsLines,
                                   bool useMediaId, string filePath, string mediaId,
                                   string miniTitle)
        {
            switch (msgType)
            {
                case "text" when string.IsNullOrWhiteSpace(textContent):
                    throw new Exception("文本内容不能为空");
                case "markdown" when string.IsNullOrWhiteSpace(markdownContent):
                    throw new Exception("Markdown 内容不能为空");
                case "textcard" when string.IsNullOrWhiteSpace(cardTitle) || string.IsNullOrWhiteSpace(cardUrl):
                    throw new Exception("文本卡片标题和链接不能为空");
                case "news" when string.IsNullOrWhiteSpace(newsLines):
                    throw new Exception("图文内容不能为空");
                case "image" or "voice" or "video" or "file":
                    if (!useMediaId)
                    {
                        if (string.IsNullOrWhiteSpace(filePath)) throw new Exception("文件路径(含扩展名)不能为空");
                        if (!File.Exists(filePath)) throw new Exception("文件不存在：" + filePath);
                    }
                    else
                    {
                        if (string.IsNullOrWhiteSpace(mediaId)) throw new Exception("media_id 不能为空");
                    }
                    break;
                case "miniprogram_notice" when string.IsNullOrWhiteSpace(miniTitle):
                    throw new Exception("小程序通知标题不能为空");
            }
        }
        #endregion

        #region 执行入口
        public async Task<ExecuteResult> ExecuteAsync(IServerCommandExecuteContext ctx)
        {
            try
            {
                /* 1. 异步解析所有公式属性 */
                var corpId = (await ctx.EvaluateFormulaAsync(CorpId))?.ToString();
                var secret = (await ctx.EvaluateFormulaAsync(Secret))?.ToString();
                var agentId = (await ctx.EvaluateFormulaAsync(AgentId))?.ToString();
                var touser = (await ctx.EvaluateFormulaAsync(Touser))?.ToString();
                var toparty = (await ctx.EvaluateFormulaAsync(Toparty))?.ToString();
                var filePath = (await ctx.EvaluateFormulaAsync(FilePath))?.ToString();
                var mediaId = (await ctx.EvaluateFormulaAsync(MediaId))?.ToString();
                var msgType = Msgtype?.ToLower();

                /* 2. 运行时数据校验 */
                Validate(msgType,
                         TextContent, MarkdownContent,
                         CardTitle, CardUrl, NewsLines,
                         UseMediaId, filePath, mediaId,
                         MiniTitle);

                /* 3. 取 access_token */
                string token = await GetAccessTokenAsync(corpId, secret);

                /* 4. 处理媒体上传（无需临时文件，直接使用原文件） */
                string finalMediaId = string.Empty;
                string absPath = string.Empty;
                string uploadHeaders = string.Empty;
                string uploadBody = string.Empty;

                if (new[] { "image", "voice", "video", "file" }.Contains(msgType))
                {
                    if (!UseMediaId)
                    {
                        absPath = Path.GetFullPath(filePath);

                        // ➜ 直接上传原文件
                        var (ok, result, headers, body) = await UploadMediaAsync(absPath, msgType, corpId, secret);
                        uploadHeaders = headers;
                        uploadBody = body;

                        if (!ok)
                        {
                            ctx.Parameters[ResultTo] = BuildFinalJson(result, token, "", absPath, msgType, uploadHeaders, uploadBody);
                            return new ExecuteResult();
                        }
                        finalMediaId = result;
                    }
                    else
                    {
                        finalMediaId = mediaId;
                        absPath = "";
                    }
                }

                /* 5. 构造消息体并发送 */
                string bodyJson = BuildJsonBody(msgType,
                                              touser, toparty, agentId,
                                              TextContent, MarkdownContent,
                                              CardTitle, CardDesc, CardUrl,
                                              NewsLines, MiniTitle, MiniPage,
                                              finalMediaId);
                string sendUrl = $"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={token}";

                using var http = new HttpClient();
                var resp = await http.PostAsync(sendUrl,
                          new StringContent(bodyJson, System.Text.Encoding.UTF8, "application/json"));
                string rawResp = await resp.Content.ReadAsStringAsync();

                ctx.Parameters[ResultTo] = BuildFinalJson(rawResp, token, finalMediaId, absPath, msgType, uploadHeaders, uploadBody);
                return new ExecuteResult();
            }
            catch (Exception ex)
            {
                var fakeResp = JsonConvert.SerializeObject(new { errcode = -1, errmsg = ex.Message });
                ctx.Parameters[ResultTo] = BuildFinalJson(fakeResp, "", "", "", Msgtype?.ToLower(), "", "");
                return new ExecuteResult();
            }
        }

        public ExecuteResult Execute(IServerCommandExecuteContext ctx) => ExecuteAsync(ctx).GetAwaiter().GetResult();
        public override string ToString() => "发送企业微信消息增强版";
        public override CommandScope GetCommandScope() => CommandScope.ExecutableInServer;
        #endregion

        #region 私有工具
        private static readonly Dictionary<string, (string Token, DateTime ExpireAt)> Cache = new();

        private static async Task<string> GetAccessTokenAsync(string corpId, string secret)
        {
            var key = $"{corpId}#{secret}";
            if (Cache.TryGetValue(key, out var item) && item.ExpireAt > DateTime.UtcNow.AddMinutes(1))
                return item.Token;

            var url = $"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpId}&corpsecret={secret}";
            using var http = new HttpClient();
            var json = await http.GetStringAsync(url);
            dynamic obj = JsonConvert.DeserializeObject(json);
            if ((int)obj.errcode != 0) throw new Exception("获取 access_token 失败：" + obj.errmsg);
            var token = (string)obj.access_token;
            Cache[key] = (token, DateTime.UtcNow.AddSeconds((int)obj.expires_in - 60));
            return token;
        }


        /// <summary>
        /// 上传企业微信临时素材（文件名正常、无引号 boundary、带 Content-Length）
        /// </summary>
        /// <summary>
        /// 上传企业微信临时素材（文件名与原路径完全一致，无引号 boundary，带 Content-Length）
        /// </summary>
        /// <summary>
        /// 上传临时素材（文件名与原路径完全一致）
        /// </summary>
        private static async Task<(bool ok, string result, string headers, string body)> UploadMediaAsync(
            string filePath, string type, string corpId, string secret)
        {
            if (!File.Exists(filePath))
                return (false, JsonConvert.SerializeObject(new { errcode = -1, errmsg = $"文件不存在：{filePath}" }), "", "");

            var fi = new FileInfo(filePath);
            if (fi.Length == 0)
                return (false, JsonConvert.SerializeObject(new { errcode = 44001, errmsg = "文件大小为 0" }), "", "");

            string token = await GetAccessTokenAsync(corpId, secret);
            string url = $"https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={token}&type={type}";

            string displayName = Path.GetFileName(filePath);   // 原文件名
            string boundary = Guid.NewGuid().ToString("N");

            // 构造 multipart 报文
            var sb = new StringBuilder();
            sb.AppendLine($"--{boundary}");
            sb.AppendLine($"Content-Disposition: form-data; name=\"media\"; filename=\"{displayName}\"");
            sb.AppendLine("Content-Type: application/octet-stream");
            sb.AppendLine();

            byte[] headerBytes = Encoding.UTF8.GetBytes(sb.ToString());
            byte[] fileBytes = await File.ReadAllBytesAsync(filePath);
            byte[] footerBytes = Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n");

            using var bodyStream = new MemoryStream();
            bodyStream.Write(headerBytes, 0, headerBytes.Length);
            bodyStream.Write(fileBytes, 0, fileBytes.Length);
            bodyStream.Write(footerBytes, 0, footerBytes.Length);
            bodyStream.Position = 0;

            var content = new StreamContent(bodyStream);
            content.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data")
            {
                Parameters = { new NameValueHeaderValue("boundary", boundary) }
            };
            content.Headers.ContentLength = bodyStream.Length;

            using var http = new HttpClient();
            var resp = await http.PostAsync(url, content);
            var respStr = await resp.Content.ReadAsStringAsync();

            dynamic obj = JsonConvert.DeserializeObject(respStr);
            string mediaIdOnly = (int)obj.errcode == 0 ? (string)obj.media_id : string.Empty;

            var logHeader = $"POST {url}\r\nContent-Type: multipart/form-data; boundary={boundary}\r\n";
            var logBody = $"--{boundary}\r\n" +
                            $"Content-Disposition: form-data; name=\"media\"; filename=\"{displayName}\"\r\n" +
                            $"Content-Type: application/octet-stream\r\n\r\n" +
                            $"[二进制文件内容 - {fi.Length} 字节]\r\n" +
                            $"--{boundary}--\r\n";

            return ((int)obj.errcode == 0, mediaIdOnly, logHeader, logBody);
        }






        private static string GetImageContentType(string extension)
        {
            return extension.ToLower() switch
            {
                ".jpg" or ".jpeg" => "image/jpeg",
                ".png" => "image/png",
                ".gif" => "image/gif",
                ".bmp" => "image/bmp",
                _ => "application/octet-stream"
            };
        }

        private static string BuildJsonBody(string t,
                                        string touser, string toparty, string agentId,
                                        string textContent, string markdownContent,
                                        string cardTitle, string cardDesc, string cardUrl,
                                        string newsLines, string miniTitle, string miniPage,
                                        string mediaId)
        {
            touser = string.IsNullOrWhiteSpace(touser) ? null : touser;
            toparty = string.IsNullOrWhiteSpace(toparty) ? null : toparty;

            return t switch
            {
                "text" => JsonConvert.SerializeObject(
                    new { touser, toparty, msgtype = t, agentid = agentId, text = new { content = textContent } }),
                "textcard" => new JObject
                {
                    ["touser"] = touser,
                    ["toparty"] = toparty,
                    ["msgtype"] = t,
                    ["agentid"] = agentId,
                    ["textcard"] = new JObject
                    {
                        ["title"] = cardTitle,
                        ["description"] = new JRaw($"\"{cardDesc?.Replace("\r\n", "<br/>").Replace("\n", "<br/>")}\""),
                        ["url"] = cardUrl
                    }
                }.ToString(Formatting.None),
                "news" => JsonConvert.SerializeObject(new
                {
                    touser,
                    toparty,
                    msgtype = t,
                    agentid = agentId,
                    news = new
                    {
                        articles = newsLines.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries)
                                          .Select(line => line.Split('|'))
                                          .Select(arr => new
                                          {
                                              title = arr.ElementAtOrDefault(0),
                                              description = arr.ElementAtOrDefault(1),
                                              url = arr.ElementAtOrDefault(2),
                                              picurl = arr.ElementAtOrDefault(3)
                                          }).ToList()
                    }
                }),
                "markdown" => JsonConvert.SerializeObject(new
                {
                    touser,
                    toparty,
                    msgtype = t,
                    agentid = agentId,
                    markdown = new { content = markdownContent?.Replace("\r\n", "<br/>").Replace("\n", "<br/>") }
                }),
                "miniprogram_notice" => JsonConvert.SerializeObject(new
                {
                    touser,
                    toparty,
                    msgtype = t,
                    agentid = agentId,
                    miniprogram_notice = new { appid = agentId, title = miniTitle, page = miniPage }
                }),
                "image" => JsonConvert.SerializeObject(
                    new { touser, toparty, msgtype = t, agentid = agentId, image = new { media_id = mediaId } }),
                "voice" => JsonConvert.SerializeObject(
                    new { touser, toparty, msgtype = t, agentid = agentId, voice = new { media_id = mediaId } }),
                "video" => JsonConvert.SerializeObject(
                    new { touser, toparty, msgtype = t, agentid = agentId, video = new { media_id = mediaId } }),
                "file" => JsonConvert.SerializeObject(
                    new { touser, toparty, msgtype = t, agentid = agentId, file = new { media_id = mediaId } }),
                _ => throw new NotSupportedException($"不支持的消息类型：{t}")
            };
        }

        private static string BuildFinalJson(string raw, string token, string mediaId, string absPath, string msgType, string uploadHeaders, string uploadBody)
        {
            var escapedRaw = raw?.Replace("\\", "\\\\");
            return JsonConvert.SerializeObject(new
            {
                rawResp = escapedRaw,
                msgtype = msgType,
                uploadInfo = new
                {
                    headers = uploadHeaders,
                    body = uploadBody
                },
                respContent = JsonConvert.DeserializeObject(escapedRaw ?? "{}"),
                access_Token = token,
                media_id = mediaId,
                filePath = absPath
            }, Formatting.Indented);
        }
        #endregion
    }
}